<template>
	<div style="display: inline-block;" :id='idName' @click='generate'>
		<slot>导出</slot>
	</div>
</template>

<script setup lang="ts">
	import download from './download';
	import { useAttrs } from 'vue';
	const $attrs = useAttrs();
	const props = withDefaults(defineProps<{
		// mime type [xls, csv]
		type ?: string;
		// Json下载
		data ?: any[];
		// Json对象中要导出的字段，如果未给定，则导出Json中的所有属性
		fields ?: Record<string, any>;
		// 用于修复使用变量字段的其他组件的问题，如vee-validate。exportFields的工作原理与fields完全相同
		exportFields ?: Record<string, any>;
		// 当行没有字段值时用作回退
		defaultValue ?: string;
		// 数据的标题，可以是字符串或字符串数组（多个标题）
		header ?: string | string[];
		// 数据的页脚，可以是字符串或字符串数组（多个页脚）
		footer ?: string | string[];
		// filename to export
		name ?: string;
		// 在下载之前回调获取数据，如果它被设置，它会在鼠标按下后和下载之前立即运行。重要提示:只有在没有定义数据道具的情况下才有效。
		fetch ?: Function;
		meta ?: any[];
		// 工作表选项卡的名称
		worksheet ?: string;
		// 在生成/获取数据之前调用一个方法，例如:显示加载进度
		beforeGenerate ?: Function;
		// 回调在下载框弹出之前调用一个方法，例如:隐藏加载进度
		beforeFinish ?: Function;
		// 这将转义CSV值，以修复一些数字字段的excel问题。但这会用=" and "包装每个csv数据，为了避免你必须将该道具设置为false。默认值:真正的
		escapeCsv ?: boolean;
		// 字符串化长数字和小数(解决数字准确性损失的问题)，默认值:false
		stringifyLongNum ?: boolean;
	}>(), {
		type: 'xls',
		data: () => [],
		fields: () => ({}),
		exportFields: () => ({}),
		defaultValue: '',
		name: 'data.xls',
		meta: () => [],
		worksheet: 'Sheet1',
		escapeCsv: true,
		stringifyLongNum: false
	});

	const idName = computed(() => {
		let now = new Date().getTime();
		return `export_${now}`;
	});

	const downloadFields = computed(() => {
		if (props.fields) return props.fields;

		if (props.exportFields) return props.exportFields;
	});

	const generate = async () => {
		if (typeof props.beforeGenerate === 'function') {
			await props.beforeGenerate();
		}
		let { data } = props;
		if (typeof props.fetch === 'function' || !data) data = await props.fetch();

		if (!data || !data.length) {
			return;
		}

		let json = getProcessedJson(data, downloadFields.value);
		if (props.type === 'html') {
			// 这主要用于测试
			return exportFile(
				jsonToXLS(json),
				props.name.replace('.xls', '.html'),
				'text/html'
			);
		} else if (props.type === 'csv') {
			return exportFile(
				jsonToCSV(json),
				props.name.replace('.xls', '.csv'),
				'application/csv'
			);
		}
		return exportFile(
			jsonToXLS(json),
			props.name,
			'application/vnd.ms-excel'
		);
	};

	const exportFile = async (data : string, filename : string, mime : string) => {
		let blob = base64ToBlob(data, mime);
		if (typeof props.beforeFinish === 'function') await props.beforeFinish();
		download(blob, filename, mime);
	}
	/*
		   jsonToXLS
		   ---------------
		   Transform json data into an xml document with MS Excel format, sadly
		   it shows a prompt when it opens, that is a default behavior for
		   Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
		   */
	const jsonToXLS = (data) => {
		let xlsTemp =
			'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>'
		let xlsData = '<thead>'
		const colspan = Object.keys(data[0]).length
		// Header
		const header = props.header || $attrs.title
		if (header) {
			xlsData += parseExtraData(
				header,
				`<tr><th colspan="${colspan}">\${data}</th></tr>`
			)
		}

		// Fields
		xlsData += '<tr>'
		for (let key in data[0]) {
			xlsData += `<th>${key}</th>`
		}
		xlsData += '</tr>'
		xlsData += '</thead>'

		// Data
		xlsData += '<tbody>'
		data.map(function (item, index) {
			xlsData += '<tr>'
			for (let key in item) {
				xlsData +=
					`<td>${preprocessLongNum(
						valueReformattedForMultilines(item[key])
					)
					}</td>`
			}
			xlsData += '</tr>'
		})
		xlsData += '</tbody>'

		// Footer
		if (props.footer != null) {
			xlsData += '<tfoot>'
			xlsData += parseExtraData(
				props.footer,
				`<tr><td colspan="${colspan}">\${data}</td></tr>`
			)
			xlsData += '</tfoot>'
		}

		return xlsTemp
			.replace('${table}', xlsData)
			.replace('${worksheet}', props.worksheet)
	}

	/*
	jsonToCSV
	---------------
	Transform json data into an CSV file.
	*/
	const jsonToCSV = (data) => {
		let csvData = []

		// Header
		const header = props.header || $attrs.title
		if (header) {
			csvData.push(parseExtraData(header, '${data}\r\n'))
		}

		// Fields
		for (let key in data[0]) {
			csvData.push(key)
			csvData.push(',')
		}
		csvData.pop()
		csvData.push('\r\n')
		// Data
		data.map(function (item) {
			for (let key in item) {
				let escapedCSV = `${item[key]}`
				// Escaped CSV data to string to avoid problems with numbers or other types of values
				// this is controlled by the prop escapeCsv
				if (props.escapeCsv) {
					escapedCSV = `="${escapedCSV}"` // cast Numbers to string
					if (escapedCSV.match(/[,"\n]/)) {
						escapedCSV = `"${escapedCSV.replace(/\"/g, '""')}"`
					}
				}
				csvData.push(escapedCSV)
				csvData.push(',')
			}
			csvData.pop()
			csvData.push('\r\n')
		})
		// Footer
		if (props.footer != null) {
			csvData.push(parseExtraData(props.footer, '${data}\r\n'))
		}
		return csvData.join('')
	}
	/*
	getProcessedJson
	---------------
	Get only the data to export, if no fields are set return all the data
	*/
	const getProcessedJson = (data, header) => {
		let keys = getKeys(data, header)
		let newData = []
		data.map(function (item, index) {
			let newItem = {}
			for (let label in keys) {
				let property = keys[label]
				newItem[label] = getValue(property, item)
			}
			newData.push(newItem)
		})

		return newData
	}
	const getKeys = (data, header) => {
		if (header) {
			return header
		}

		let keys = {}
		for (let key in data[0]) {
			keys[key] = key
		}
		return keys
	}
	/*
	parseExtraData
	---------------
	Parse title and footer attribute to the csv format
	*/
	const parseExtraData = (extraData, format) => {
		let parseData = ''
		if (Array.isArray(extraData)) {
			for (let i = 0; i < extraData.length; i++) {
				if (extraData[i])
					parseData += format.replace('${data}', extraData[i])
			}
		} else {
			parseData += format.replace('${data}', extraData)
		}
		return parseData
	}

	const getValue = (key, item) => {
		const field = typeof key !== 'object' ? key : key.field
		let indexes = typeof field !== 'string' ? [] : field.split('.')
		let value = props.defaultValue

		if (!field) value = item
		else if (indexes.length > 1)
			value = getValueFromNestedItem(item, indexes)
		else value = parseValue(item[field])

		if (key.hasOwnProperty('callback'))
			value = getValueFromCallback(value, key.callback)

		return value
	}

	/*
		convert values with newline \n characters into <br/>
		*/
	const valueReformattedForMultilines = (value) => {
		if (typeof value == 'string') return value.replace(/\n/gi, '<br/>')
		else return value
	}
	const preprocessLongNum = (value) => {
		if (props.stringifyLongNum) {
			if (String(value).startsWith('0x')) {
				return value
			}
			if (!isNaN(value) && value != '') {
				if (value > 99999999999 || value < 0.0000000000001) {
					return `="${value}"`
				}
			}
		}
		return value
	}
	const getValueFromNestedItem = (item, indexes) => {
		let nestedItem = item
		for (let index of indexes) {
			if (nestedItem) nestedItem = nestedItem[index]
		}
		return parseValue(nestedItem)
	}

	const getValueFromCallback = (item, callback) => {
		if (typeof callback !== 'function') return props.defaultValue
		const value = callback(item)
		return parseValue(value)
	}
	const parseValue = (value) => {
		return value || value === 0 || typeof value === 'boolean' ?
			value :
			props.defaultValue
	}
	const base64ToBlob = (data, mime) => {
		let base64 = window.btoa(window.unescape(encodeURIComponent(data)))
		let bstr = atob(base64)
		let n = bstr.length
		let u8arr = new Uint8ClampedArray(n)
		while (n--) {
			u8arr[n] = bstr.charCodeAt(n)
		}
		return new Blob([u8arr], {
			type: mime
		})
	}
</script>